package net.aiion.weave.impl.xml;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.Comment;
import javax.xml.stream.events.DTD;
import javax.xml.stream.events.EndDocument;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.Namespace;
import javax.xml.stream.events.ProcessingInstruction;
import javax.xml.stream.events.StartDocument;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import net.aiion.weave.spi.xml.XmlContext;
import net.aiion.weave.spi.xml.XmlException;
import net.aiion.weave.spi.xml.XmlQualifiedName;
import net.aiion.weave.spi.xml.XmlSink;
import net.aiion.weave.spi.xml.events.AttributeXmlEvent;
import net.aiion.weave.spi.xml.events.CharactersXmlEvent;
import net.aiion.weave.spi.xml.events.CommentXmlEvent;
import net.aiion.weave.spi.xml.events.DocumentStartXmlEvent;
import net.aiion.weave.spi.xml.events.DocumentTypeDefinitionXmlEvent;
import net.aiion.weave.spi.xml.events.ElementEndXmlEvent;
import net.aiion.weave.spi.xml.events.ElementStartXmlEvent;
import net.aiion.weave.spi.xml.events.NamespaceXmlEvent;
import net.aiion.weave.spi.xml.events.ProcessingInstructionXmlEvent;
import net.aiion.weave.spi.xml.events.XmlEvent;

public class XmlOutputStreamWriterSinkImpl implements XmlSink
{
    private static final String WOODSTOX_PROPERTY_OUTPUT_VALIDATION = "com.ctc.wstx.outputValidateStructure";

    private OutputStream outputStream;
    private XMLEventFactory xmlEventFactory;
    private XMLEventWriter xmlEventWriter;

    public XmlOutputStreamWriterSinkImpl(final OutputStream aOutputStream) {
        outputStream = aOutputStream;
        try {
            xmlEventFactory = XMLEventFactory.newInstance();
            final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance();
            if (xmlOutputFactory.isPropertySupported(WOODSTOX_PROPERTY_OUTPUT_VALIDATION)) {
                xmlOutputFactory.setProperty(WOODSTOX_PROPERTY_OUTPUT_VALIDATION, Boolean.FALSE);
            }
            xmlEventWriter = xmlOutputFactory.createXMLEventWriter(aOutputStream);
        } catch (final XMLStreamException | FactoryConfigurationError e) {
            throw new XmlException(e);
        }
    }

    @Override
    public void handleXmlEvent(final XmlContext aXmlContext) {
        final XMLEvent xmlEvent = adaptEvent(aXmlContext.getXmlEvent());
        if (xmlEvent != null) {
            try {
                xmlEventWriter.add(xmlEvent);
                xmlEventWriter.flush();
            } catch (final XMLStreamException e) {
                throw new XmlException(e);
            }
        }
    }

    private void writeRawData(final String aRawCharacterData) {
        try {
            ensureLastTagClosed();
            outputStream.write(aRawCharacterData.getBytes());
            outputStream.flush();
        } catch (final IOException e) {
            throw new XmlException(e);
        }
    }

    private void ensureLastTagClosed() {
        try {
            xmlEventWriter.add(xmlEventFactory.createCharacters(""));
            xmlEventWriter.flush();
        } catch (final XMLStreamException e) {
            throw new XmlException(e);
        }
    }

    private XMLEvent adaptEvent(final XmlEvent aXmlEvent) {
        final XMLEvent xmlEvent;
        switch (aXmlEvent.getType()) {
        case XmlEvent.START_DOCUMENT:
            xmlEvent = adaptStartDocument((DocumentStartXmlEvent) aXmlEvent);
            break;
        case XmlEvent.DOCUMENT_TYPE_DEFINITION:
            xmlEvent = adaptDocumentTypeDefinition((DocumentTypeDefinitionXmlEvent) aXmlEvent);
            break;
        case XmlEvent.COMMENT:
            xmlEvent = adaptComment((CommentXmlEvent) aXmlEvent);
            break;
        case XmlEvent.PROCESSING_INSTRUCTION:
            xmlEvent = adaptProcessingInstruction((ProcessingInstructionXmlEvent) aXmlEvent);
            break;
        case XmlEvent.START_ELEMENT:
            xmlEvent = adaptStartElement((ElementStartXmlEvent) aXmlEvent);
            break;
        case XmlEvent.ATTRIBUTE:
            xmlEvent = adaptAttribute((AttributeXmlEvent) aXmlEvent);
            break;
        case XmlEvent.CHARACTERS:
            xmlEvent = adaptCharacters((CharactersXmlEvent) aXmlEvent);
            break;
        case XmlEvent.END_ELEMENT:
            xmlEvent = adaptEndElement((ElementEndXmlEvent) aXmlEvent);
            break;
        case XmlEvent.END_DOCUMENT:
            xmlEvent = adaptEndDocument();
            break;
        default:
            throw new XmlException("Unsupported event type " + aXmlEvent.getType());
        }

        return xmlEvent;
    }

    private StartDocument adaptStartDocument(final DocumentStartXmlEvent aXmlStartDocument) {
        return xmlEventFactory.createStartDocument(aXmlStartDocument.getEncoding(), aXmlStartDocument.getVersion(),
            aXmlStartDocument.isStandalone());
    }

    private DTD adaptDocumentTypeDefinition(final DocumentTypeDefinitionXmlEvent aXmlDocumentTypeDefinition) {
        return xmlEventFactory.createDTD(aXmlDocumentTypeDefinition.getDocumentTypeDefinition());
    }

    private Comment adaptComment(final CommentXmlEvent aXmlComment) {
        return xmlEventFactory.createComment(aXmlComment.getComment());
    }

    private ProcessingInstruction adaptProcessingInstruction(
        final ProcessingInstructionXmlEvent aXmlProcessingInstruction)
    {
        return xmlEventFactory.createProcessingInstruction(aXmlProcessingInstruction.getTarget(),
            aXmlProcessingInstruction.getData());
    }

    private StartElement adaptStartElement(final ElementStartXmlEvent aXmlStartElement) {
        final XmlQualifiedName xmlStartElementQualifiedName = aXmlStartElement.getName();
        final Collection<Attribute> attributes = new ArrayList<>();
        for (AttributeXmlEvent xmlAttribute : aXmlStartElement.getAttributes()) {
            attributes.add(adaptAttribute(xmlAttribute));
        }

        final Collection<Namespace> namespaces = new ArrayList<>();
        for (NamespaceXmlEvent xmlNamespace : aXmlStartElement.getNamespaces()) {
            namespaces.add(xmlEventFactory.createNamespace(xmlNamespace.getPrefix(), xmlNamespace.getNamespaceURI()));
        }

        return xmlEventFactory.createStartElement(xmlStartElementQualifiedName.getPrefix(),
            xmlStartElementQualifiedName.getNamespaceURI(), xmlStartElementQualifiedName.getLocalName(),
            attributes.iterator(), namespaces.iterator());
    }

    private Attribute adaptAttribute(final AttributeXmlEvent aXmlAttribute) {
        final XmlQualifiedName xmlAttributeName = aXmlAttribute.getName();
        return xmlEventFactory.createAttribute(xmlAttributeName.getPrefix(), xmlAttributeName.getNamespaceURI(),
            xmlAttributeName.getLocalName(), aXmlAttribute.getValue());
    }

    private Characters adaptCharacters(final CharactersXmlEvent aXmlCharacters) {
        if (aXmlCharacters.isRawCharacterData()) {
            writeRawData(aXmlCharacters.getData());
            return null;
        }

        if (aXmlCharacters.isUnparsedCharacterData()) {
            return xmlEventFactory.createCData(aXmlCharacters.getData());
        }

        return xmlEventFactory.createCharacters(aXmlCharacters.getData());
    }

    private EndElement adaptEndElement(final ElementEndXmlEvent aXmlEndElement) {
        final XmlQualifiedName xmlEndElementQualifiedName = aXmlEndElement.getName();
        return xmlEventFactory.createEndElement(xmlEndElementQualifiedName.getPrefix(),
            xmlEndElementQualifiedName.getNamespaceURI(), xmlEndElementQualifiedName.getLocalName());
    }

    private EndDocument adaptEndDocument() {
        return xmlEventFactory.createEndDocument();
    }
}
